package cipher.io;

import java.io.EOFException;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;

import cipher.util.CipherConst;
import cipher.util.IFCrypt;

public class CipherRandomAccessFile {
	private RandomAccessFile rfile;
	private IFCrypt cipher = null;
	private boolean write = false;
	private long vfp = 0;
	private long vlen = 0;
	private int dpos = 0;
	private int dlen = 0;
	private CipherFile file;
	private boolean sizechange = false;
	private static byte dummy[] = new byte[0];
	byte _tmpR[] = new byte[CipherConst.enc_kBufferSize];
	byte dbuf[] = null;

	private byte[] singlewritebuf = new byte[CipherConst.kBufferSize];
	private byte[] wbuf = new byte[CipherConst.kBufferSize];
	private static  byte[] bulk = new byte[CipherConst.kBufferSize];
	private int singlewritepos = 0;
	private int wpos = 0;

	private FileChannel channel;

	public CipherRandomAccessFile(CipherFile file, String isWritemode)
			throws FileNotFoundException {
		this.file = file;
		this.cipher = file.getCipher();
		this.rfile = new RandomAccessFile(file.getPhysicalFile(), isWritemode);
		this.vlen = file.length();
		if (isWritemode.indexOf("w") > 0) {
			this.write = true;
		}
	}

	public FileChannel getChannel() throws IOException {
		if (channel == null)
			channel = new CipherFileChannel(rfile.getChannel(), file, write,
					true);
		return channel;
	}

	public void close() throws IOException {
		syncBuffer();
		if (sizechange) {
			file.getPhysicalFile().setLastModified(System.currentTimeMillis());
		}
		try {
			if (channel != null)
				channel.close();
			rfile.close();
			file.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	public int read() throws IOException {
		if (dbuf != null && dpos < dlen) {
			vfp++;
		} else {
			int r = read_1(null, 0, 1);
			if (r == -1)
				return -1;
		}
		return dbuf[dpos++] & 0xFF;
	}

	public int read(byte[] buf) throws IOException {
		return read_1(buf, 0, buf.length);
	}

	public int read(byte[] buf, int offset, int len) throws IOException {
		try {
			int rtn = read_1(buf, offset, len);
			return rtn;
		} catch (Throwable e) {
			e.printStackTrace();
		}
		return -1;
	}

	long t = 0;

	private int read_1(byte[] buf, int offset, int len) throws IOException {
		syncBuffer();
		long rp = vfp + len;
		if (vfp == vlen) return -1;
		if (rp > vlen) {
			rp = vlen;
			len = (int) (vlen - vfp);
		}
		
		int orglen = len;
		int bpos = 0;
		if (dpos > 0 && dpos < dlen) {
			int r = (dlen - dpos);
			int rest = r > len ? len : r;
			System.arraycopy(dbuf, dpos, buf, offset, rest);
			len -= rest;
			dpos += rest;
			bpos += rest;
			vfp += rest;
			if (len == 0){
				return orglen;
			}
		}

		long sp = CipherConst.getDecodedLastBlockPosition(vfp);
		long esp = CipherConst.convert2encodedBlockPosition(sp);
		rfile.seek(esp);
		int read = rfile.read(_tmpR);
		dbuf = cipher.decrypt(_tmpR, 0, read);
		if (dbuf != null) {
			dpos = 0;
			dlen = dbuf.length;
			int of = (int) (vfp - sp);
			int rest = dlen - of;
			int clen = rest > len ? len : rest;
			if (buf != null) {
				System.arraycopy(dbuf, of, buf, bpos + offset, clen);
				dpos = of + clen;
			} else {
				dpos = of;
			}
			len -= clen;
			bpos += clen;
			vfp += clen;
		}
		while (len > 0) {
			read = rfile.read(_tmpR);
			dbuf = cipher.decrypt(_tmpR, 0, read);
			if (dbuf != null) {
				dlen = dbuf.length;
				dpos = 0;
				int rest = dlen > len ? len : dlen;
				if (buf != null) {
					System.arraycopy(dbuf, 0, buf, offset + bpos, rest);
					dpos += rest;
				}
				len -= rest;
				bpos += rest;
				vfp += rest;
			}
		}
		return orglen;
	}

	public void seek(long pos) throws IOException {
		vfp = pos;
		dpos = dlen;
	}

	private void syncBuffer() throws IOException {
		if (singlewritepos > 0) {
			write_1(singlewritebuf, 0, singlewritepos);
			singlewritepos = 0;
		}
	}

	public void write(int b) throws IOException {
		if (!write)
			throw new IOException("Invalid argument");
		if (singlewritebuf == null) {
			singlewritebuf = new byte[CipherConst.kBufferSize];
		}
		if (singlewritepos == CipherConst.kBufferSize) {
			syncBuffer();
		}
		singlewritebuf[singlewritepos++] = (byte) b;
	}

	public void write(byte[] buf) throws IOException {
		if (!write)
			throw new IOException("Invalid argument");
		syncBuffer();
		write_1(buf, 0, buf.length);
	}

	public void write(byte[] buf, int offset, int len) throws IOException {
		if (!write)
			throw new IOException("Invalid argument");
		syncBuffer();
		write_1(buf, offset, len);
	}

	private void write_1(byte[] buf, int offset, int len) throws IOException {
		sizechange = true;
System.out.println("wrtie "+offset+" len "+len+" vfp "+vfp);
		long rp = vfp + len;
		long sp = CipherConst.getDecodedLastBlockPosition(vfp);
		long ep = CipherConst.getDecodedLastBlockPosition(rp);
		long esp = CipherConst.convert2encodedBlockPosition(sp);

		long bend = rp == ep ? rp : ep + CipherConst.kBufferSize;
		long bcopylen = vfp - sp;
		long acopylen = bend - rp;

		try {
			if (acopylen > 0 && ep + CipherConst.kBufferSize >= file.length()) {
				if (rp < file.length()) {
					acopylen = file.length() - rp;
				} else {
					acopylen = 0;
				}
			}
			if (bcopylen > 0) {
				rfile.seek(esp);
				int r = rfile.read(_tmpR);
				cipher.decrypt(dummy, 0, 0);
				byte beforedec[] = cipher.decrypt(_tmpR, 0, r);
				if (beforedec != null) {
					System.arraycopy(beforedec, 0, wbuf, 0, (int) bcopylen);
				}else{
					System.arraycopy(bulk, 0, wbuf, 0, (int) bcopylen);
				}
				wpos = (int) bcopylen;
			}
			rfile.seek(esp);
			int bpos = 0;
			while (len > 0) {
				int rb = CipherConst.kBufferSize - wpos;
				int clen = rb > len ? len : rb;
				System.arraycopy(buf, offset + bpos, wbuf, wpos, clen);
				bpos += clen;
				len -= clen;
				wpos += clen;
				if (wpos == CipherConst.kBufferSize) {
					byte p[] = cipher.encrypt(wbuf, 0, wpos);
					rfile.write(p, 0, p.length);
					wpos = 0;
				}
			}
			long lastp = rfile.getFilePointer();
			if (acopylen > 0) {
				long eep = CipherConst.convert2encodedBlockPosition(ep);
				rfile.seek(eep);
				int r = rfile.read(_tmpR);
				cipher.decrypt(dummy, 0, 0);
				byte afterdec[] = cipher.decrypt(_tmpR, 0, r);
				if (afterdec != null) {
					System.arraycopy(afterdec, (int) (rp - ep), wbuf, wpos,	(int) acopylen);
				}else{
					System.arraycopy(bulk, 0, wbuf, wpos,	(int) acopylen);
				}
				wpos += (int) acopylen;
			}
			System.out.println("WPOS "+wpos);
			if (wpos > 0) {
				rfile.seek(lastp);
				byte p[] = cipher.encrypt(wbuf, 0, wpos);
				rfile.write(p, 0, p.length);
				wpos = 0;
			}
			vfp += bpos;
			if (vfp > vlen)
				vlen = vfp;
			file.setLength(vlen);
			file.setEncLength(CipherConst.getEcodeFullValue(vlen));
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	public long getFilePointer() throws IOException {
		return vfp;

	}

	public long length() throws IOException {
		return vlen;
	}

	public void setLength(long length) throws IOException {
		if (!write)
			throw new IOException("Invalid argument");
		System.out.println("setLength "+length);
		syncBuffer();
		long newEncLength = CipherConst.getEcodeFullValue(length);
		if (length < vlen) { // 파일 크기가 작아졌을경우
			long sp = CipherConst.getDecodedLastBlockPosition(length); 
			long esp = CipherConst.convert2encodedBlockPosition(sp); 
			rfile.seek(esp); // 위치로 이동
			int r = rfile.read(_tmpR);
			rfile.setLength(newEncLength); // 물리적 파일의 크기를 변경
			if (r > 0 && sp != length) { // 읽은 크기가 0 보다 크고 파일 크기가 마지막 블록 위치 다를경우만
				cipher.decrypt(dummy, 0, 0);
				byte d[] = cipher.decrypt(_tmpR, 0, r);
				if (d != null) {
					rfile.seek(esp);
					cipher.encrypt(dummy, 0, 0);
					byte e[] = cipher.encrypt(d, 0, (int) (length - sp));
					if (e != null) {
						rfile.write(e, 0, e.length);
					}
				}
			}
		} else if (length > vlen) { // 파일 크기가 커졌을 경우
			long vl = CipherConst.getDecodedLastBlockPosition(vlen);
			long evl = CipherConst.convert2encodedBlockPosition(vl);
			rfile.seek(evl);
			int r = rfile.read(_tmpR);
			rfile.setLength(newEncLength);
			if (r > 0) {
				cipher.decrypt(dummy, 0, 0);
				byte d[] = cipher.decrypt(_tmpR, 0, r);
				if (d != null) {
					int _dlen = (int) (length - vl) > CipherConst.kBufferSize ? CipherConst.kBufferSize
							: (int) (length - vl);
					byte t[] = new byte[_dlen];
					System.arraycopy(d, 0, t, 0, d.length);
					rfile.seek(evl);
					cipher.encrypt(dummy, 0, 0);
					byte e[] = cipher.encrypt(t, 0, t.length);
					if (e != null) {
						rfile.write(e, 0, e.length);
					}
				}
			}
		} else { // 변화가 없을 경우
			return;
		}
		file.setEncLength(newEncLength);
		file.setLength(length);
		vlen = length;
	}

	public int skipBytes(int n) {
		try {
			syncBuffer();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		dpos = dlen;
		vfp += n;
		return n;
	}

	/* FROM RANDOMACCESS */
	public final boolean readBoolean() throws IOException {
		int ch = this.read();
		if (ch < 0)
			throw new EOFException();
		return (ch != 0);
	}

	public final byte readByte() throws IOException {
		int ch = this.read();
		if (ch < 0)
			throw new EOFException();
		return (byte) (ch);
	}

	public final int readUnsignedByte() throws IOException {
		int ch = this.read();
		if (ch < 0)
			throw new EOFException();
		return ch;
	}

	public final short readShort() throws IOException {
		int ch1 = this.read();
		int ch2 = this.read();
		if ((ch1 | ch2) < 0)
			throw new EOFException();
		return (short) ((ch1 << 8) + (ch2 << 0));
	}

	public final int readUnsignedShort() throws IOException {
		int ch1 = this.read();
		int ch2 = this.read();
		if ((ch1 | ch2) < 0)
			throw new EOFException();
		return (ch1 << 8) + (ch2 << 0);
	}

	public final char readChar() throws IOException {
		int ch1 = this.read();
		int ch2 = this.read();
		if ((ch1 | ch2) < 0)
			throw new EOFException();
		return (char) ((ch1 << 8) + (ch2 << 0));
	}

	public final int readInt() throws IOException {
		int ch1 = this.read();
		int ch2 = this.read();
		int ch3 = this.read();
		int ch4 = this.read();
		if ((ch1 | ch2 | ch3 | ch4) < 0)
			throw new EOFException();
		return ((ch1 << 24) + (ch2 << 16) + (ch3 << 8) + (ch4 << 0));
	}

	public final long readLong() throws IOException {
		return ((long) (readInt()) << 32) + (readInt() & 0xFFFFFFFFL);
	}

	public final float readFloat() throws IOException {
		return Float.intBitsToFloat(readInt());
	}

	public final double readDouble() throws IOException {
		return Double.longBitsToDouble(readLong());
	}

	public final String readLine() throws IOException {
		StringBuffer input = new StringBuffer();
		int c = -1;
		boolean eol = false;
		while (!eol) {
			switch (c = read()) {
			case -1:
			case '\n':
				eol = true;
				break;
			case '\r':
				eol = true;
				long cur = getFilePointer();
				if ((read()) != '\n') {
					seek(cur);
				}
				break;
			default:
				input.append((char) c);
				break;
			}
		}
		if ((c == -1) && (input.length() == 0)) {
			return null;
		}
		return input.toString();
	}

	public final void writeBoolean(boolean v) throws IOException {
		write(v ? 1 : 0);
	}

	public final void writeByte(int v) throws IOException {
		write(v);

	}

	public final void writeShort(int v) throws IOException {
		write((v >>> 8) & 0xFF);
		write((v >>> 0) & 0xFF);
	}

	public final void writeChar(int v) throws IOException {
		write((v >>> 8) & 0xFF);
		write((v >>> 0) & 0xFF);
	}

	public final void writeInt(int v) throws IOException {
		write((v >>> 24) & 0xFF);
		write((v >>> 16) & 0xFF);
		write((v >>> 8) & 0xFF);
		write((v >>> 0) & 0xFF);

	}

	public final void writeLong(long v) throws IOException {
		write((int) (v >>> 56) & 0xFF);
		write((int) (v >>> 48) & 0xFF);
		write((int) (v >>> 40) & 0xFF);
		write((int) (v >>> 32) & 0xFF);
		write((int) (v >>> 24) & 0xFF);
		write((int) (v >>> 16) & 0xFF);
		write((int) (v >>> 8) & 0xFF);
		write((int) (v >>> 0) & 0xFF);
	}

	public final void writeFloat(float v) throws IOException {
		writeInt(Float.floatToIntBits(v));
	}

	public final void writeDouble(double v) throws IOException {
		writeLong(Double.doubleToLongBits(v));
	}

	public final void writeBytes(String s) throws IOException {
		int len = s.length();
		byte[] b = new byte[len];
		s.getBytes(0, len, b, 0);
		write(b, 0, len);
	}

	public final void writeChars(String s) throws IOException {
		int clen = s.length();
		int blen = 2 * clen;
		byte[] b = new byte[blen];
		char[] c = new char[clen];
		s.getChars(0, clen, c, 0);
		for (int i = 0, j = 0; i < clen; i++) {
			b[j++] = (byte) (c[i] >>> 8);
			b[j++] = (byte) (c[i] >>> 0);
		}
		write(b, 0, blen);
	}

	public final void writeUTF(String str) throws IOException {
		// DataOutputStream.writeUTF(str, this);
	}

}
